微服务配置
...

把整套spring-cloud-dependencies依赖放在dependencyManagement里,可以在Maven Reopsitory网站查对应版本包,用哪个再去加哪个依赖

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

springboot 2.7用2021.0.5

网关 Gateway
...

核心概念
...

路由
...

  • 路由是构建网关的基本模块,由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由

断言
...

  • 可以匹配HTTP请求中的所有内容(如请求头和请求参数),如果请求和断言相匹配则进行路由

过滤
...

  • 即Spring框架中的GatewayFilter实例,即过滤器

在这里插入图片描述

Gateway工作流程的核心逻辑即 路由转发+执行过滤器链

启动配置
...

  1. 加依赖

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
    
    • 添加gateway依赖就不用添加spring-boot-starter-web了
  2. 用yml配置

    server:
      port: 9527									  #网关端口
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          routes:
            - id: t1                                  #路由id,没有规则但要求唯一,应配合服务名
              uri: http://localhost:8080              #匹配后提供服务的路由地址
              predicates:
                - Path=/t/**                          #断言,路径相匹配的进行路由
    
    • 此时即可用localhost:9527/t访问localhost:8080/t的服务
    • 网关配置允许跨域,微服务不配置,则要求所有请求统一走网关
    • 网关配置允许跨域,微服务配置允许跨域,需要在网关的配置里加上重复请求头。配置DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE。此时走不走网关,都没有跨域问题

    跨域配置:

    spring:
      cloud:
        gateway:
          globalcors:
            cors-configurations:
              '[/**]':
                # 允许携带认证信息
                # 允许跨域的源(网站域名/ip),设置*为全部
                # 允许跨域请求里的head字段,设置*为全部
                # 允许跨域的method, 默认为GET和OPTIONS,设置*为全部
                # 跨域允许的有效期
                allow-credentials: true
                allowed-origins:
                  - "http://localhost:*"
                allowed-headers: "*"
                allowed-methods:
                  - OPTIONS
                  - GET
                  - POST
                max-age: 3600
    
  3. 用bean配置

    和yml配置选其一种即可

    @Bean
        public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
            RouteLocatorBuilder.Builder routes = builder.routes();
            routes.route("route_t",r -> r.path("/t").uri("http://localhost:8080/t")).build();
            return routes.build();
        }
    

动态路由
...

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

  • 需要注册中心组件eureka

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
    
  1. yml配置添加

    spring:
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true
    
  2. yml中路由的uri换成匹配后提供服务的路由地址lb://serviceName,是gateway自动创建的负载均衡uri

断言
...

Path Route Predicate
...

匹配指定路径下的请求,可以是具体的请求,也可使用/**表示匹配所有子级请求

- Path=/user/**

After Route Predicate
...

匹配在指定日期时间之后发生的请求

- After=2020-02-05T15:10:03.685+08:00[Asia/Shanghai]

可以使用java类获取当前时间 2022-10-22T15:53:48.304+08:00[Asia/Shanghai]

ZonedDateTime now = ZonedDateTime.now();

Before Route Predicate
...

匹配在指定日期时间之前发生的请求

- Before=2017-01-20T17:42:47.789-07:00[America/Denver]

Between Route Predicate
...

匹配在指定日期时间之间发生的请求

- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

需要两个参数,一个是 Cookie name ,一个是cookie的value的正则表达式。
路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行

- Cookie=cokieName, \d+

img

Header Route Predicate
...

由两个参数组成,第一个参数为Header名称,第二参数为Header的Value值的正则表达式,表示:匹配指定名称且其值与正则表达式匹配的Header的请求

- Header=headerName, \d+

在这里插入图片描述

Host Route Predicate
...

参数为请求的Host地址,多个参数使用逗号分割,设置的Host地址可以使用**表示通配符

- Host=**.somehost.org,**.anotherhost.org

配置中匹配的Host,可以匹配以somehost.org或者anotherhost.org结尾的Host地址,其他Host地址访问会出现错误。

在这里插入图片描述

Method Route Predicate
...

由一个或多个HTTP Method组成,比如:POST、PUT、GET、DELETE

 - Method=GET,POST

Query Route Predicate
...

由两个参数组成,第一个参数为参数名称,第二参数为参数的值(满足正则即可),表示:匹配指定名称且其值与正则表达式匹配的带参的请求

- Query=name,\d+

在这里插入图片描述

RemoteAddr Route Predicate
...

参数由CIDR 表示法(IPv4 或 IPv6)字符串组成。配置中可以匹配IP为192.168.1.100的值,如果不满足192.168.1.1/24的IP规则,会出现错误。

- RemoteAddr=192.168.1.1/24

Weight Route Predicate
...

由group和weight(权重数值)组成,表示将相同的请求根据权重跳转到不同的uri地址,要求group的名称必须一致

- id: weight_high
        uri: https://weighthigh.org
        predicates:
        - Weight=groupName, 8
      - id: weight_low
        uri: https://weightlow.org
        predicates:
        - Weight=groupName, 2

该路由会将约 80% 的流量转发到weighthigh.org,将约 20% 的流量转发到weightlow.org

过滤器
...

分类
...

自定义过滤器
...

GlobalFilter
...

继承GlobalFilter,Ordered接口

@Component
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
    //过滤器执行事件
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange);
    }
    //过滤器放的位置,数值越小越靠前
    @Override
    public int getOrder() {
        return 0;
    }
}

ServerWebExchange存储了Request和Response对象和一些拓展方法。

配置中心 Config
...

为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置

服务端
...

也称为分布式配置中心,是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口

客户端
...

通过制定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取加载配置信息,配置服务器。默认采用git存储配置信息,并可以通过git客户端方便管理和访问配置内容

服务端搭建
...

  1. github新建仓库,复制git地址(git@github.com:LoliWolf/ShopPlatform.git),本地建git仓库并clone

  2. 新建Module,即为配置中心模块cloudConfig Center

  3. pom配置

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
            </dependency>
    
  4. yml配置

    spring:
      application:
        name: eureka0  #注册进eureka服务器的微服务名
      cloud:
        config:
          server:
            git:
              uri: git@github.com:LoliWolf/ShopPlatform.git
              #搜索目录
              search-paths: ShopPlatform
          #读取分支
          label: master
    
  5. 配置读取规则

    上步设置了默认分支为master

    • /{label(分支)}/{application(配置文件名)}-{profile(发布版本)}.yml

      http://config-3344.com:3344/master/comfig-prod.yml

      http://config-3344.com:3344/master/comfig-test.yml

    • /{application}-{profile}.yml

      使用设置的默认分支

    • /{application}/{profile}[/{label}]

客户端搭建
...

  1. 创客户端Moudle,添加依赖

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
    
  2. 新建bootstrap.yml配置文件

    bootstrap.yml配置文件优先级更高,优先加载,不会被本地配置覆盖

  3. bootstrap.yml配置

    spring:
      application:
        name: config-client #eureka注册的名字
      cloud:
        config:
          label: master  #分支名称
          name: config  #配置文件名称
          profile: dev  #后缀名称
          uri: http://localhost:3344 #配置中心地址,多个用逗号分割
    

    即可在启动微服务时获取到配置

  4. 手动动态刷新客户端配置

    • 客户端moudle加监控依赖

      <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-actuator</artifactId>
              </dependency>
      
    • 暴露监控端口

      management:
        endpoints:
          web:
            exposure:
              include: "*"
      
    • 在需要配置动态刷新配置的类上添加@RefreshScope注解

    • 对配置客户端/actuator/refresh发POST请求,刷新配置

注册中心 Eureka
...

Eureka是服务注册与发现的组件

CAP原则
...

在一个分布式系统中,一致性(Consistency),可用性(Availability),分区容错性(Partition tolerance)三个要素最多只能同时实现两点,不可能三者兼顾。

  • 一致性C:整个集群数据一致
  • 可用性A:当有一个节点挂掉了,整个集群可以继续对外提供服务
  • 分区容错性P(不可避免):由于机房网络或分区会导致各个机器数据短暂不一致

CP:数据是一致的但是如果有个节点挂了,整个服务在几分钟内不能提供服务

AP:数据可能是不一致的,但有节点挂了服务依然可用

eureka遵循AP,zookeeper遵循CP

服务端搭建
...

注册中心:可以让别人注册,也可以注册自己

  1. 建项目,pom加依赖

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            </dependency>
    
  2. yml配置

    server:
      port: 8761    #Eureka服务器默认端口
    spring:
      application:
        name: eureka-server    #应用名称
    
  3. 启动类添加注解@EnableEurekaServer

客户端搭建
...

  1. 模块pom加依赖

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  2. yml配置

    server:
      port: 8080
    spring:
      application:
        name: eureka-client-a    #要在server注册的服务名
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka    #eureka-server地址
    
  3. 启动类加注解@EnableEurekaClient/@EnableDiscoveryClient

  • 同一服务可以注册多个实例

    把一个服务启动配置复制一份在program arguments里加--server.port=即可

    image-20221024224957063

配置
...

eureka配置分服务器(server),客户端(client),实例(instance)

服务端需要配置server,instance,客户端需要配置client,instance

# 是否允许开启自我保护模式,缺省:true
# 当Eureka服务器在短时间内丢失过多客户端时,自我保护模式可使服务端不再删除失去连接的客户端
eureka.server.enable-self-preservation = false
 
# Peer节点更新间隔,单位:毫秒
eureka.server.peer-eureka-nodes-update-interval-ms = 
 
# Eureka服务器清理无效节点的时间间隔,单位:毫秒,缺省:60000,即60秒
eureka.server.eviction-interval-timer-in-ms = 60000
# 服务名,默认取 spring.application.name 配置值,如果没有则为 unknown
eureka.instance.appname = eureka-client
 
# 实例ID
eureka.instance.instance-id = eureka-client-instance1
 
# 应用实例主机名
eureka.instance.hostname = localhost
 
# 客户端在注册时使用自己的IP而不是主机名,缺省:false
eureka.instance.prefer-ip-address = false
 
# 应用实例IP
eureka.instance.ip-address = 127.0.0.1
 
# 服务失效时间,失效的服务将被剔除。单位:秒,默认:90
eureka.instance.lease-expiration-duration-in-seconds = 90
 
# 服务续约(心跳)频率,单位:秒,缺省30
eureka.instance.lease-renewal-interval-in-seconds = 30
 
# 状态页面的URL,相对路径,默认使用 HTTP 访问,如需使用 HTTPS则要使用绝对路径配置,缺省:/info
eureka.instance.status-page-url-path = /info
 
# 健康检查页面的URL,相对路径,默认使用 HTTP 访问,如需使用 HTTPS则要使用绝对路径配置,缺省:/health
eureka.instance.health-check-url-path = /health
# Eureka服务器的地址,类型为HashMap,缺省的Key为 defaultZone;缺省的Value为 http://localhost:8761/eureka
# 如果服务注册中心为高可用集群时,多个注册中心地址以逗号分隔。 
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka
 
# 是否向注册中心注册自己,缺省:true
# 一般情况下,Eureka服务端是不需要再注册自己的
eureka.client.register-with-eureka = true
 
# 是否从Eureka获取注册信息,缺省:true
# 一般情况下,Eureka服务端是不需要的
eureka.client.fetch-registry = true
 
# 客户端拉取服务注册信息间隔,单位:秒,缺省:30
eureka.client.registry-fetch-interval-seconds = 30
 
# 是否启用客户端健康检查
eureka.client.health-check.enabled = true
 
# 获取集群中最新的server节点数据频率  单位:秒 缺省:5min
eureka.client.eureka-service-url-poll-interval-seconds = 60
 
# 连接Eureka服务器的超时时间,单位:秒,缺省:5
eureka.client.eureka-server-connect-timeout-seconds = 5
 
# 从Eureka服务器读取信息的超时时间,单位:秒,缺省:8
eureka.client.eureka-server-read-timeout-seconds = 8
 
# 获取实例时是否只保留状态为 UP 的实例,缺省:true
eureka.client.filter-only-up-instances = true
 
# Eureka服务端连接空闲时的关闭时间,单位:秒,缺省:30
eureka.client.eureka-connection-idle-timeout-seconds = 30
 
# 从Eureka客户端到所有Eureka服务端的连接总数,缺省:200
eureka.client.eureka-server-total-connections = 200
 
# 从Eureka客户端到每个Eureka服务主机的连接总数,缺省:50
eureka.client.eureka-server-total-connections-per-host = 50

注册中心集群
...

eureka采用去中心化模式,在集群中对数据进行广播和扩散

配置各个注册中心实例注册自己,将其他注册中心实例地址添加到配置中

  client:
    service-url:
      defaultZone: http://192.168.12.3:8761/eureka,http://192.168.14.4:8761/eureka
    register-with-eureka: true

实例主机名如果相同,如都是localhost,不会被认为是集群

在客户端添加一个注册中心地址即可在注册中心集群广播扩散

集群配置例子
...

不配置主机名

把所有注册中心实例地址全部注册进所有注册中心的client.service-url(注意改端口),启动时候用参数修改端口

客户端也注册所有注册中心实例地址

服务发现
...

即通过服务应用名,找到具体服务信息

可在代码中注入DiscoveryClient,获取服务列表

注册中心 Zookeeper
...

客户端搭建
...

  1. 加依赖,启动类加注解 @EnableDiscoveryClient

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </dependency>
    
  2. 配置文件

    spring:
      cloud:
        zookeeper:
    #      zk地址
          connect-string: localhost:2181
    #      会话过期时间
          session-timeout: 60000
    #      连接超时时间
          connection-timeout: 15000
    #      初始重试等待时间
          base-sleep-time-ms: 1000
    #      最大重试次数
          max-retries: 10
    #      发现客户端
          discovery:
            enabled: true
    
    

注册中心 Consul
...

客户端搭建
...

  1. 加依赖加注解@EnableDiscoveryClient

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    
  2. 配置

    spring:
      application:
        name: conusle-provider
      cloud:
        consul:
          host: localhost
          port: 8500
        # 服务发现配置
          discovery:
            # 启用服务发现
            enabled: true
            # 启用服务注册
            register: true
            # 服务停止时取消注册
            deregister: true
            # 表示注册时使用IP而不是hostname
            prefer-ip-address: true
            ip-address: 192.168.10.132
            # 执行监控检查的频率
            health-check-interval: 30s
            # 设置健康检查失败多长时间后,取消注册
            health-check-critical-timeout: 30s
            # 健康检查的路径
            health-check-path: /actuator/info
            # 服务注册标识,格式为:应用名称+服务器IP+端口
            instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
            # 服务名
            service-name: ${spring.application.name}
    

从注册中心检索服务和实例数据
...

@Autowired
private DiscoveryClient discoveryClient;

public String serviceUrl() {
    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    if (list != null && list.size() > 0 ) {
        return list.get(0).getUri().toString();
    }
    return null;
}

负载均衡 Ribbon
...

Spring Cloud 2020.0.0版本彻底删除掉了Netflix除Eureka外的所有组件。

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-ribbon</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>

配置方式
...

Ribbon默认使用Eureka服务发现,在删除Netflix组件后SpringCloud使用Loadbalancer代替

Ribbon分为全局配置和客户端配置,如果添加客户端配置可以在ribbon上级添加application name

# 全局配置
ribbon:
  ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
  ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
  OkToRetryOnAllOperations: true #对超时请求启用重试机制
  MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
  MaxAutoRetries: 1 # 切换实例后重试最大次数
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
# 局部配置
user-service:
  ribbon:

负载均衡策略的选择:

  • RandomRule:从提供服务的实例中以随机的方式;
  • RoundRobinRule:以线性轮询的方式,就是维护一个计数器,从提供服务的实例中按顺序选取,第一次选第一个,第二次选第二个,以此类推,到最后一个以后再从头来过;
  • RetryRule:在RoundRobinRule的基础上添加重试机制,即在指定的重试时间内,反复使用线性轮询策略来选择可用实例;
  • WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择;
  • BestAvailableRule:选择并发较小的实例;
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例;
  • ZoneAwareLoadBalancer:采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。

整合Eureka自动配置
...

服务发现和注册自动完成,直接用[RestTemplate](../SpringBoot/SpringBoot.md# 使用RestTemplate发请求)调服务名就能实现负载均衡

uri用"http://"+ {application_name} +"服务接口地址"

脱离Eureka配置
...

  1. 使用Consul做服务发现

  2. 配置文件配置服务地址

    CLIENT:
      ribbon:
        # 不使用eureka服务发现
        eureka:
          enabled: false
      	# 服务列表
        listOfServers: localhost:8001,localhost:8002
        # 服务列表刷新间隔
        ServerListRefreshInterval: 5000
        # 负载策略
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
        # Ping策略
        NFLoadBalancerPingClassName: com.netflix.loadbalancer.PingUrl
        # 设置它的服务实例信息来自配置文件, 如果不设置NIWSServerListClassName就会去euereka里面找
        NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    

重试机制
...

  spring:
  	cloud:
   	  loadbalancer:
     	 retry:
        	enable: true

负载均衡 LoadBalancer
...

自定义实例选择器
...

如果针对不同的服务使用各自的配置,则可选择在配置类中配置实例选择器

public class CustomLoadBalancerConfiguration {
    //实例选择器配置
    @Bean
    public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(ConfigurableApplicationContext context){
        return ServiceInstanceListSupplier.builder()
                //使用服务发现
                .withDiscoveryClient()
                //使用健康检查
                .withHealthChecks()
                //···
                .build(context);
    }
}

不同服务的配置:

spring:
  cloud:
    loadbalancer:
      clients:
        my-service:

切换策略
...

默认使用轮询,内置随机RandomLoadBalancer,轮询RoundRobinLoadBalancer。

新增配置类,不要加@Configuration

public class CustomLoadBalancerConfiguration {

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

添加配置类,手动选择配置

@Configuration
//value的值指定了在给定的loadbalancer client配置下,要发送请求到哪个服务(服务名)
@LoadBalancerClient(value = "stores", configuration = CustomLoadBalancerConfiguration.class)
public class MyConfiguration {
    @Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }
}
//配置多个
//@LoadBalancerClients({
//    @LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class),
//    @LoadBalancerClient(value = "customers", configuration = CustomersLoadBalancerClientConfiguration.class)
//})

自定义策略
...

仿照官方的RandomLoadBalancer,修改getInstanceResponse()进行实现

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import reactor.core.publisher.Mono;

public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(RandomLoadBalancer.class);
    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public RandomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> {
            return this.processInstanceResponse(supplier, serviceInstances);
        });
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
        }

        return serviceInstanceResponse;
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }

            return new EmptyResponse();
        } else { //此处实现自己的负载均衡策略
            int index = ThreadLocalRandom.current().nextInt(instances.size());
            ServiceInstance instance = (ServiceInstance)instances.get(index);
            return new DefaultResponse(instance);
        }
    }
}

单独使用
...

配合[RestTemplate/WebClient](../SpringBoot/SpringBoot.md# 使用RestTemplate发请求),做配置类Bean并在使用时注入

	@Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }
	@Bean
	@LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

健康检查
...

如果已使用服务发现,通常不需要添加这种健康检查机制,因为我们直接从服务注册中心检索实例的健康状态。

spring:
  cloud:
    loadbalancer:
      health-check:
#       创建延迟
        initial-delay: 10s
#       检查间隔
        interval: 100s
#       健康检查使用的url
        path:
          default: /actuator/health
          service-a: /health
          service-b: /health
#       健康检查使用的端口,未指定则为服务实例上的可用端口
        port: 8081
#       服务列表的刷新一般由代理实现,例如服务发现。如果不想使用代理来刷新服务列表,可以使用以下配置使服务列表的刷新由HealthCheckServiceInstanceListSupplier操作。
#       开启刷新服务列表
        refetch-instances: true
#       设置刷新间隔
        refetch-instances-interval: 100s
#       取消额外的健康检查(每个服务实例的刷新也会触发一次健康检查)
        repeat-health-check: false

其他功能
...

优先选择先前实例
...

配置选择器:

.withSameInstancePreference()

全局默认:

spring:
  cloud:
    loadbalancer:
      configurations: same-instance-preference

优先选择请求cookie中提供的实例
...

配置选择器:

.withRequestBasedStickySession()

全局默认:

spring:
  cloud:
    loadbalancer:
      configurations: request-based-sticky-session
      sticky-session:
#       是否在发送请求前更新服务所选实例(如果原始请求cookie中的服务实例不可用,则该服务实例可能与原始请求cookie中的服务实例不同)
        add-service-instance-cookie: true
#       自定义cookie名字,默认为 sc-lb-instance-id
        instance-id-cookie-name: sc-lb-instance-id

服务调用 OpenFeign
...

由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。内嵌Ribbon实现负载均衡。新版202x的openfeign整合移除了Ribbon,改用LoadBalancer

旧版Ribbon用法
...

springboot版本不大于2.3.X,使用springcloud版本不大于Hoxton,服务发现使用eureka

使用方法
...

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  1. 启动类加注解@EnableFeignClients开启功能

  2. 服务发现配置

# 消费者可不注册进Eureka
eureka:
  client:
    register-with-eureka: false
#   Eureka服务端地址
    service-url:
      defaultZone: http://127.0.0.1:8001/eureka,http://localhost:8002/eureka
  1. 制作Service接口
@Service
//  上下文标识,服务提供者的微服务应用名
@FeignClient(contextId = "service-name",value = "spring-application-name")
public interface TestService {
//  服务提供者的服务调用地址
    @GetMapping("/provider/test/{id}", headers = {"Content-Type=application/json;charset=UTF-8",···})
    Object testService(@PathVariable("id") Long id);
}
  1. 制作Controller,注入Service直接调用。OpenFeign直接从名字对应的微服务调用对应接口
  • @FeignClient详细参数:

    value/name:服务提供者的应用名

    contextId:被用作Bean名称,做配置时可以用;如果多个service都用了同一个服务提供者,value重复,则会因为Bean名称冲突报错,解决方案一:添加配置spring.main.allow-bean-definition-overriding=true,解决方案二:指定不同的contextId作为Bean名字(获取Bean名字规则:contextId>value>name);

    url:手动指定feign调用地址,一般用在调试,也可以在配置文件中设置feign.client.config.testClient.url。这么提供url无法使用负载均衡

    path:当前FeignClient访问接口的统一前缀,效果等同控制器类上加RequestMapping

    decode404:调用发生404时,若该参数为true,则执行decoder解码,否则抛异常

    fallback:定义容错处理类,如果抛异常了则执行,该类必须实现。无法知道熔断异常信息

    @Component
    public class UserRemoteClientFallback implements UserRemoteClient {
    	@Override
    	public User getUser(int id) {
    		return new User(0, "默认fallback");
    	}
    }
    
    @FeignClient(value = "optimization-user", fallback = UserRemoteClientFallback.class)
    public interface UserRemoteClient {
    	
    	@GetMapping("/user/get")
    	public User getUser(@RequestParam("id")int id);
    	
    }
    

    fallbackFactory:同上,可以知道熔断异常信息

    @Component
    public class UserRemoteClientFallbackFactory implements FallbackFactory<UserRemoteClient> {
    	private Logger logger = LoggerFactory.getLogger(UserRemoteClientFallbackFactory.class);
    	
    	@Override
    	public UserRemoteClient create(Throwable cause) {
    		return new UserRemoteClient() {
    			@Override
    			public User getUser(int id) {
    				logger.error("UserRemoteClient.getUser异常", cause);
    				return new User(0, "默认");
    			}
    		};
    	}
    }
    
    @FeignClient(value = "optimization-user", fallbackFactory = UserRemoteClientFallbackFactory.class)
    public interface UserRemoteClient {
    	
    	@GetMapping("/user/get")
    	public User getUser(@RequestParam("id")int id);
    	
    }
    

    primary:是否将该FeignClient作为主bean(默认true)。若FeignClient有多个相同的Bean在Spring容器中,当我们在使用@Autowired进行注入的时候,不知道注入哪个,此时需要声明。

    qualifier:如果相同的Bean的primary都是false,autowired会不知道注入哪个,此时用qualifier声明

    // Feign Client定义
    @FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
    public interface UserRemoteClient {
    	
    	@GetMapping("/get")
    	public User getUser(@RequestParam("id") int id);
    }
    // Feign Client注入
    @Autowired
    @Qualifier("userRemoteClient")
    private UserRemoteClient userRemoteClient;
    

    configuration:自定义FeignClient配置类,此类配置类不需要加@Configuration

超时控制
...

# 使用feign做单/全局服务配置 service-name即服务里设置的上下文标识
feign:
  client:
    config:
      service-name/default:
	  # 请求处理超时时间
        read-timeout: 6000
      # 连接超时时间
        connect-timeout: 1000
# 使用ribbon做全局配置(旧版)
ribbon:
  ReadTimeout: 6000
  ConnectTimeout: 5000

日志记录
...

  • 日志等级:

    NONE:默认的,不显示任何日志;
    BASIC:仅记录请求方法、URL、响应状态码及执行时间;
    HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息
    FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据
    
  • 单个服务日志记录

    logging:
      level:
        com.demo.feign.FeignDemo: debug
    # 下面的配置,也可以做配置类代替
    # @Bean
    # public Logger.Level level() { return Logger.Level.FULL; }
    feign:
      client:
        config:
          default:
            loggerLevel: full
    
  • 全局日志记录

    logging:
      level:
        feign.Logger: debug
    # 下面的配置,也可以做配置类代替
    # @Bean
    # public Logger.Level level() { return Logger.Level.FULL; }
    feign:
      client:
        config:
          default:
            loggerLevel: full
    

添加Headers
...

@PostMapping(value = "/card")
public CardVo createCard(@RequestBody CardDto condition, @RequestHeader MultiValueMap<String, String> headers);

@PostMapping(value = "/book/api", headers = {"Content-Type=application/json;charset=UTF-8", "App-Secret=${app.secret}"})
void saveBook(@RequestBody BookDto condition);

自定义拦截器
...

做拦截器实现RequestInterceptor接口

feign.client.config.service-name.request-interceptors=com.demo.interceptor.CustomInterceptor

使用缓存
...

如果开启@EnableCaching,则会给CachingCapability注册一个bean,使feign客户端识别接口上的注解

@GetMapping("/demo/{filterParam}")
@Cacheable(cacheNames = "demo-cache", key = "#keyParam")
String demoEndpoint(String keyParam, @PathVariable String filterParam);

最佳实践
...

将OpenFeign抽离出来做一个module,直接pom引包使用

服务降级 Hystrix
...

于springcloud2020移除

服务降级
...

降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。

生产者端
...

主启动类开启@EnableHystrix

在service方法上添加降级配置,降级方法参数要对应

@HystrixCommand(fallbackMethod = "paymentTimeOutHandler",commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
    })
//超时1000ms

public String paymentTimeOutHandler(Integer id){
        return "TimeOut";
    }

消费者端
...

由于使用了openfeign,没有service实体类,将降级配置换到controller上,同上

设置默认配置
...

直接注解在类上

@DefaultProperties(defaultFallback = "defaultExceptionHandler",commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")})

在要加降级的方法上直接加@HystrixCommand

整合在openfeign
...

做feign服务的实现类作为降级处理类

@FeignClient(value = "PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentServiceFallback.class)
feign:
  hystrix:
    enabled: true
  client:
    config:
      default:
        read-timeout: 5000
        connect-timeout: 1000

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000

服务熔断
...

当服务发生错误到达一定频次,可以触发熔断,请求不再打到下级微服务,直接调fallback处理并返回

注解中添加配置
...

@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),//触发所需请求数量下限
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),//服务错误比例
@HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000")//断路器开启时长

整合在openfeign
...

hystrix:
  command:
    default:
      circuitBreaker:
        enabled: true
        errorThresholdPercentage: 50
        requestVolumeThreshold: 3
        sleepWindowInMilliseconds: 8000

dashboard
...

消息总线 Bus
...

设计思想:利用消息总线触发一个服务端ConfigServer的/actuator/busrefresh端点,而刷新所有客户端的配置

Config服务端
...

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
spring: # rabbitmq配置
  rabbitmq:
    host: 
    port: 
    username: 
    password: 
management:
  endpoints:
    web:
      exposure:
        include: "busrefresh" # 暴露/actuator/busrefresh端点

Config客户端
...

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

mq配置添加在bootstrap

spring: # rabbitmq配置
  rabbitmq:
    host: 
    port: 
    username: 
    password: 

定点通知
...

在路径中设置目标实例/busrefresh/{destination}

destination默认是spring.application.name:server.port,也可以用spring.cloud.bus.id单独配置

按服务通知
...

端口号换来选中所有同名的服务

消息驱动 Stream
...

屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
stream通过Binder对象与消息中间件交互
Binder是应用与消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,Rab bitMQ的exchange),这些都可以通过配置文件来实现
Channel:队列Queue的抽象,实现存储和转发的媒介,通过Channel对队列进行配置
Source/Sink:发送者/接收者
image.png

常用注解
...

  • @EnableBinding()
    将信道channel和exchange绑定在一起
  • @Input()
    标识输入通道,接收消息进入应用
  • @Output()
    标识输出通道,发布的消息从此离开
  • @StreamListener()
    监听队列,用于消费者队列的消息接收

配置信息
...

spring:  
  application:  
    name: stream-provider  
  cloud:  
    stream:  
      binders: #要绑定的rabbitmq服务信息  
        rabbit: #自定义的Binder名称  
          type: rabbit #binder类型  
          environment: #环境配置  
            spring:  
              rabbitmq:  
                host: wolf.bj.cn  
                port: 5672  
                username: admin  
                password: 8a57gw72uri  
      bindings: # 服务整合处理  
        output: # 自定义通道名称  
          destination: Exchange #消息目标的交换机名  
          content-type: application/json #消息类型(json)  
          binder: rabbit #指定该通道使用的binder  
        output2: # 自定义通道名称  
          destination: Exchange2 #消息目标的交换机名  
          content-type: text/plain #消息类型(文本)  
          binder: rabbit #指定该通道使用的binder

连RabbitMQ
...

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

注解式开发
...

生产者
...

import com.demo.service.IMessageProvider;  
import org.springframework.cloud.stream.annotation.EnableBinding;  
import org.springframework.cloud.stream.messaging.Source;  
import org.springframework.messaging.MessageChannel;  
import org.springframework.messaging.support.MessageBuilder;  
import javax.annotation.Resource;  
  
@EnableBinding(Source.class) // 定义消息的输出管道  
public class MessageProviderImpl implements IMessageProvider {  
  
    @Resource  
    private MessageChannel output; // 消息发送管道  
  
    @Override  
    public String send() {  
        output.send(MessageBuilder.withPayload("消息").build());  
        return null;    }  
}

消费者
...

函数式开发
...